#!/usr/bin/env python

# Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

"""
Find an Android device with a given ABI.

The name of the Android device is printed to stdout.

Optionally configure and launch an emulator if there's no existing device for a
given ABI. Will download and install Android SDK components as needed.
"""

import optparse
import os
import re
import sys
import traceback
import utils


DEBUG = False
VERBOSE = False


def BuildOptions():
  result = optparse.OptionParser()
  result.add_option(
      "-a", "--abi",
      action="store", type="string",
      help="Desired ABI. armeabi-v7a or x86.")
  result.add_option(
      "-b", "--bootstrap",
      help='Bootstrap - create an emulator, installing SDK packages if needed.',
      default=False, action="store_true")
  result.add_option(
      "-d", "--debug",
      help='Turn on debugging diagnostics.',
      default=False, action="store_true")
  result.add_option(
      "-v", "--verbose",
      help='Verbose output.',
      default=False, action="store_true")
  return result


def ProcessOptions(options):
  global DEBUG
  DEBUG = options.debug
  global VERBOSE
  VERBOSE = options.verbose
  if options.abi is None:
    sys.stderr.write('--abi not specified.\n')
    return False
  return True


def ParseAndroidListSdkResult(text):
  """
  Parse the output of an 'android list sdk' command.

  Return list of (id-num, id-key, type, description).
  """
  header_regex = re.compile(
      r'Packages available for installation or update: \d+\n')
  packages = re.split(header_regex, text)
  if len(packages) != 2:
    raise utils.Error("Could not get a list of packages to install")
  entry_regex = re.compile(
      r'^id\: (\d+) or "([^"]*)"\n\s*Type\: ([^\n]*)\n\s*Desc\: (.*)')
  entries = []
  for entry in packages[1].split('----------\n'):
    match = entry_regex.match(entry)
    if match == None:
      continue
    entries.append((int(match.group(1)), match.group(2), match.group(3),
        match.group(4)))
  return entries


def AndroidListSdk():
  return ParseAndroidListSdkResult(utils.RunCommand(
      ["android", "list", "sdk", "-a", "-e"]))


def AndroidSdkFindPackage(packages, key):
  """
  Args:
    packages: list of (id-num, id-key, type, description).
    key: (id-key, type, description-prefix).
  """
  (key_id, key_type, key_description_prefix) = key
  for package in packages:
    (package_num, package_id, package_type, package_description) = package
    if (package_id == key_id and package_type == key_type
        and package_description.startswith(key_description_prefix)):
      return package
  return None


def EnsureSdkPackageInstalled(packages, key):
  """
  Makes sure the package with a given key is installed.

  key is (id-key, type, description-prefix)

  Returns True if the package was not already installed.
  """
  entry = AndroidSdkFindPackage(packages, key)
  if entry is None:
    raise utils.Error("Could not find a package for key %s" % key)
  packageId = entry[0]
  if VERBOSE:
    sys.stderr.write('Checking Android SDK package %s...\n' % str(entry))
  out = utils.RunCommand(
      ["android", "update", "sdk", "-a", "-u", "--filter", str(packageId)])
  return '\nInstalling Archives:\n' in out


def SdkPackagesForAbi(abi):
  packagesForAbi = {
    'armeabi-v7a': [
      # The platform needed to install the armeabi ABI system image:
      ('android-15', 'Platform',  'Android SDK Platform 4.0.3'),
      # The armeabi-v7a ABI system image:
      ('sysimg-15', 'SystemImage',  'Android SDK Platform 4.0.3')
    ],
    'x86': [
      # The platform needed to install the x86 ABI system image:
      ('android-15', 'Platform',  'Android SDK Platform 4.0.3'),
      # The x86 ABI system image:
      ('sysimg-15', 'SystemImage',  'Android SDK Platform 4.0.4')
    ]
  }

  if abi not in packagesForAbi:
    raise utils.Error('Unsupported abi %s' % abi)
  return packagesForAbi[abi]


def TargetForAbi(abi):
  for package in SdkPackagesForAbi(abi):
    if package[1] == 'Platform':
      return package[0]


def EnsureAndroidSdkPackagesInstalled(abi):
  """Return true if at least one package was not already installed."""
  abiPackageList = SdkPackagesForAbi(abi)
  installedSomething = False
  packages = AndroidListSdk()
  for package in abiPackageList:
    installedSomething |= EnsureSdkPackageInstalled(packages, package)
  return installedSomething


def ParseAndroidListAvdResult(text):
  """
  Parse the output of an 'android list avd' command.
  Return List of {Name: Path: Target: ABI: Skin: Sdcard:}
  """
  text = text.split('Available Android Virtual Devices:\n')[-1]
  text = text.split(
      'The following Android Virtual Devices could not be loaded:\n')[0]
  result = []
  line_re = re.compile(r'^\s*([^\:]+)\:\s*(.*)$')
  for chunk in text.split('\n---------\n'):
    entry = {}
    for line in chunk.split('\n'):
      line = line.strip()
      if len(line) == 0:
        continue
      match = line_re.match(line)
      if match is None:
        sys.stderr.write('Match fail %s\n' % str(line))
        continue
        #raise utils.Error('Match failed')
      entry[match.group(1)] = match.group(2)
    if len(entry) > 0:
      result.append(entry)
  return result


def AndroidListAvd():
  """Returns a list of available Android Virtual Devices."""
  return ParseAndroidListAvdResult(utils.RunCommand(["android", "list", "avd"]))


def FindAvd(avds, key):
  for avd in avds:
    if avd['Name'] == key:
      return avd
  return None


def CreateAvd(avdName, abi):
  out = utils.RunCommand(["android", "create", "avd", "--name", avdName,
                          "--target", TargetForAbi(abi), '--abi', abi],
                         input="no\n")
  if out.find('Created AVD ') < 0:
    if VERBOSE:
        sys.stderr.write('Could not create AVD:\n%s\n' % out)
    raise utils.Error('Could not create AVD')


def AvdExists(avdName):
  avdList = AndroidListAvd()
  return FindAvd(avdList, avdName) is not None


def EnsureAvdExists(avdName, abi):
  if AvdExists(avdName):
    return
  if VERBOSE:
    sys.stderr.write('Checking SDK packages...\n')
  if EnsureAndroidSdkPackagesInstalled(abi):
    # Installing a new package could have made a previously invalid AVD valid
    if AvdExists(avdName):
        return
  CreateAvd(avdName, abi)


def StartEmulator(abi, avdName, pollFn):
  """
  Start an emulator for a given abi and svdName.

  Echo the emulator's stderr and stdout output to our stderr.

  Call pollFn repeatedly until it returns False. Leave the emulator running
  when we return.

  Implementation note: Normally we would call the 'emulator' binary, which
  is a wrapper that launches the appropriate abi-specific emulator. But there
  is a bug that causes the emulator to exit immediately with a result code of
  -11 if run from a ssh shell or a No Machine shell. (And only if called from
  three levels of nested python scripts.) Calling the ABI-specific versions
  of the emulator directly works around this bug.
  """
  emulatorName = {'x86' : 'emulator-x86', 'armeabi-v7a': 'emulator-arm'}[abi]
  command = [emulatorName, '-avd', avdName, '-no-boot-anim', '-no-window']
  utils.RunCommand(command, pollFn=pollFn, killOnEarlyReturn=False,
          outStream=sys.stderr, errStream=sys.stderr)


def ParseAndroidDevices(text):
  """Return Dictionary [name] -> status"""
  text = text.split('List of devices attached')[-1]
  lines = [line.strip() for line in text.split('\n')]
  lines = [line for line in lines if len(line) > 0]
  devices = {}
  for line in lines:
    lineItems = line.split('\t')
    devices[lineItems[0]] = lineItems[1]
  return devices


def GetAndroidDevices():
  return ParseAndroidDevices(utils.RunCommand(["adb", "devices"]))


def FilterOfflineDevices(devices):
  online = {}
  for device in devices.keys():
    status = devices[device]
    if status != 'offline':
      online[device] = status
  return online


def GetOnlineAndroidDevices():
  return FilterOfflineDevices(GetAndroidDevices())


def GetAndroidDeviceProperty(device, property):
  return utils.RunCommand(
      ["adb", "-s", device, "shell", "getprop", property]).strip()


def GetAndroidDeviceAbis(device):
  abis = []
  for property in ['ro.product.cpu.abi', 'ro.product.cpu.abi2']:
    out = GetAndroidDeviceProperty(device, property)
    if len(out) > 0:
      abis.append(out)
  return abis


def FindAndroidRunning(abi):
  for device in GetOnlineAndroidDevices().keys():
    if abi in GetAndroidDeviceAbis(device):
      return device
  return None


def AddSdkToolsToPath():
  script_dir = os.path.dirname(sys.argv[0])
  dart_root = os.path.realpath(os.path.join(script_dir, '..', '..'))
  third_party_root = os.path.join(dart_root, 'third_party')
  android_tools = os.path.join(third_party_root, 'android_tools')
  android_sdk_root = os.path.join(android_tools, 'sdk')
  android_sdk_tools = os.path.join(android_sdk_root, 'tools')
  android_sdk_platform_tools = os.path.join(android_sdk_root, 'platform-tools')
  os.environ['PATH'] = ':'.join([
      os.environ['PATH'], android_sdk_tools, android_sdk_platform_tools])
  # Remove any environment variables that would affect our build.
  for i in ['ANDROID_NDK_ROOT', 'ANDROID_SDK_ROOT', 'ANDROID_TOOLCHAIN',
            'AR', 'BUILDTYPE', 'CC', 'CXX', 'GYP_DEFINES',
            'LD_LIBRARY_PATH', 'LINK', 'MAKEFLAGS', 'MAKELEVEL',
            'MAKEOVERRIDES', 'MFLAGS', 'NM']:
    if i in os.environ:
      del os.environ[i]


def FindAndroid(abi, bootstrap):
  if VERBOSE:
    sys.stderr.write('Looking for an Android device running abi %s...\n' % abi)
  AddSdkToolsToPath()
  device = FindAndroidRunning(abi)
  if not device:
    if bootstrap:
      if VERBOSE:
        sys.stderr.write("No emulator found, try to create one.\n")
      avdName = 'dart-build-%s' % abi
      EnsureAvdExists(avdName, abi)

      # It takes a while to start up an emulator.
      # Provide feedback while we wait.
      pollResult = [None]
      def pollFunction():
        if VERBOSE:
          sys.stderr.write('.')
        pollResult[0] = FindAndroidRunning(abi)
        # Stop polling once we have our result.
        return pollResult[0] != None
      StartEmulator(abi, avdName, pollFunction)
      device = pollResult[0]
  return device


def Main():
  # Parse options.
  parser = BuildOptions()
  (options, args) = parser.parse_args()
  if not ProcessOptions(options):
    parser.print_help()
    return 1

  # If there are additional arguments, report error and exit.
  if args:
    parser.print_help()
    return 1

  try:
    device = FindAndroid(options.abi, options.bootstrap)
    if device != None:
      sys.stdout.write("%s\n" % device)
      return 0
    else:
      if VERBOSE:
        sys.stderr.write('Could not find device\n')
      return 2
  except utils.Error as e:
    sys.stderr.write("error: %s\n" % e)
    if DEBUG:
      traceback.print_exc(file=sys.stderr)
    return -1


if __name__ == '__main__':
  sys.exit(Main())
